Yioop v10 Mail Refactor — Plan

Arc Post-Mortem Summary. This arc moved the mail logic out of the oversized SocialComponent into MailModel and the mail library and backend classes. listMessages was unified onto the backends, per-user session state (sort, sequence cache, sort-support) moved to MailModel, default-folder resolution and the unread-count walk moved onto MailBackend so SocialComponent issues no raw IMAP, and address and header parsing and validation became library and model helpers. The result is a much smaller SocialComponent with mail concerns separated out.

Two parts, in order. Fixed list; each step only gains a when done. marks the step in progress. Ongoing in every patch (not a step): inline single-use methods under 20 lines and leave no stubs in SocialComponent.

  1. Part 1 — move mail logic out of SocialComponent into MailModel and the mail library classes
    1. listMessages unified onto the backends
    2. MailModel — per-user session state (sort, sequence cache, sort-support)
    3. default-folder resolution moved onto MailBackend
    4. unread-count walk moved into the backend (SocialComponent now issues no raw IMAP)
    5. address/header parsing and validation into a library/model: userMailParseAddressList, userMailExtractBareAddress, userMailIsValidAddress, userMailCleanHeader, userMailFilterAddressList, userMailJoinAddressLists, userMailCleanEnvelopes, userMailEnvelopeSender
    6. account-form parse/validate into MailAccountModel: userMailParseAccountFields, userMailValidateAccountFields, userMailCleanAccountFormValues
      Reviewed → kept in the controller. These are controller concerns with no model/library home: parse reads the request, validate produces tl() messages, and the clean is display-prep. The model stores raw values (the IMAP/SMTP client needs raw HOST/USERNAME to connect) and the controller escapes them for the view at render time. A first attempt that moved these onto the model was reverted — the same disposition as userMailCleanEnvelopes / userMailFromOptions.
    7. compose / reply / forward / MIME building into a library builder: userMailBuildReplyPrefill, userMailReplySubject, userMailFromOptions, userMailCleanComposeValues, userMailSendIdentities
      Done: new MailComposeBuilder (replySubject + replyPrefill); userMailBuildReplyPrefill kept only the backend fetch and delegates the building. userMailSendIdentitiesMailAliasModel::identitiesFor. userMailFromOptions and userMailCleanComposeValues stay in the controller — they call $parent->clean, which a library class must not.
    8. folder-list session cache into MailModel: userMailReadCachedFolders, userMailRememberFolders, and the MAIL_FOLDERS_CACHE session state with its invalidation points
      Done: MailModel gained cachedFolders (fresh-or-null), cacheFolders (write + freshness stamp), invalidateFolders, foldersFor (raw read for the cross-account move and folder-protected check), and allCachedFolders (the FOLDERS_BY_ACCOUNT map); the MAIL_FOLDERS_CACHE_TTL const moved too. SocialComponent now issues no direct $_SESSION["MAIL_FOLDERS"] access. Part 1 complete.
  2. Part 2 — consolidate the mail library classes into src/library/mail/ (namespace seekquarry\yioop\library\mail; MailSite.php stays in atto; BulkEmailJob / MailCloneJob stay in media_jobs)
    1. relocate and re-namespace, updating all references: ImapClient, ImapMailBackend, MailBackend, MailSiteMailBackend, MailSiteFactory, SmtpClient, MimeMessage, ImapEnvelopeParser, ImapFolderListParser, ImapResponseParser, ImapListing, DkimKey, MailHeaderParser, MailRecordCache, MailScheduledDispatcher, MailUnreadProbe, MailBackendException (and MailComposeBuilder, added in 1.7)
      In progress. MailHeaderParser relocated first as the template (a leaf, no outbound mail-class deps). Established procedure per class: create src/library/mail/, git mv the file, change its namespace to seekquarry\yioop\library\mail, then update every reference — change the use import in importing files, and add a use in former same-namespace library classes that had relied on same-namespace resolution. The autoloader maps the namespace straight to the path, so no loader change is needed. Done so far: MailHeaderParser (the template), then leaves MailComposeBuilder and MailRecordCache (the latter adding a use to its former same-namespace users DkimKey, SpfCheck, DmarcCheck). The final 15 (ImapClient, the Imap parsers, ImapListing, ImapMailBackend, MailBackend, MailSiteMailBackend, MailSiteFactory, SmtpClient, MimeMessage, DkimKey, MailScheduledDispatcher, MailUnreadProbe, MailBackendException) then moved in a single pass: moving them together kept every mail-to-mail reference same-namespace, so only external referencers changed — use imports repointed, the L\ library-alias mail uses rewritten to a new ML (= library\mail) alias, one fully-qualified reference fixed, and the now-redundant same-namespace imports dropped. All 18 mail classes now live under src/library/mail/; MailSite.php stays in atto and BulkEmailJob / MailCloneJob in media_jobs. The mail email-auth checks DmarcCheck and SpfCheck (both DB-clean) were relocated too, so the whole src/library/mail/ set is in place. Part 2 complete.
  3. Part 3 — library layering cleanup: nothing in src/library may touch a model or the database. Models are the only layer that touches the database; controllers and components are the only layers that touch models. Library classes must take their data as arguments. Mail-related violations to fix (move the DB/model access into a model, have the library class receive values as parameters):
    1. MailSiteFactory instantiates MailAliasModel, UserModel, MailSenderAllowModel directly. Coupled with the authenticator item below: both are set up in MailSiteFactory::build(), which is called from the MailServer executable, SocialComponent, and two library classes (MailSiteMailBackend, MailCloneJob). The library callers cannot create models to inject, so this needs the daemon-layer decision before it can be fixed.
    2. MailBackend and MailUnreadProbe no longer reach a model. Done: the component loads the account row (new SocialComponent::userMailBackend helper) and passes it to MailBackend::forAccountId; the unread probe takes an accounts-provider closure so the lookup happens only on a cache miss; new model method MailAccountModel::getAccountsWithPassword.
    3. CredentialCipher no longer touches the database. Done: the key store moved into MailAccountModel::masterKey() (loads or mints the MAIL_SECRET key, caches it on the model); the cipher is now pure crypto whose encrypt/decrypt take the raw key as a parameter. The key-persistence test moved from CredentialCipherTest into MailAccountModelTest.
    4. YioopUserAuthenticator relocated into src/library/mail/. Done: folder move plus namespace change. This also repaired a latent break — MailSiteFactory (already in library\mail) referenced new YioopUserAuthenticator() unqualified, which had been resolving to a non-existent library\mail\YioopUserAuthenticator since the factory itself moved; the class now lives there, so the reference resolves. Its model-instantiation layering fix is still deferred with the factory below.
    5. MailSiteFactory — left as-is for now (per Chris), apart from the authenticator now resolving from mail/. Still instantiates MailAliasModel, UserModel, MailSenderAllowModel; resolving that needs the daemon-layer decision (build() is called from the MailServer executable, a component, and two library classes that cannot inject models).
    6. Note: non-mail library classes violate this too (FeedDocumentBundle, VersionFunctions, several media jobs); out of scope for the mail arc but flagged here